// jsonql-auth middleware
import {
  AUTH_TYPE,
  ISSUER_NAME,
  VALIDATOR_NAME,
  AUTH_CHECK_HEADER,
  BEARER
} from 'jsonql-constants'
import {
  chainFns,
  getDebug,
  packResult,
  headerParser,
  printError,
  isNotEmpty,
  handleOutput,
  forbiddenHandler,
  ctxErrorHandler,

  createTokenValidator, // import from the jwt

  isObject
} from '../utils'
import {
  JsonqlResolverNotFoundError,
  JsonqlAuthorisationError,
  JsonqlValidationError,
  finalCatch
} from 'jsonql-errors'
import { getLocalValidator } from 'jsonql-resolver'
import { trim } from 'lodash'

const debug = getDebug('auth-middleware')

// this will create a cache version without keep calling the getter
var validatorFn;

// declare a global variable to store the userdata with null value
// this way we don't mess up with the resolver have to check
// the last param is the user data

/**
 * @param {object} ctx Koa context
 * @param {string} type to look for
 * @return {mixed} the bearer token on success
 */
const authHeaderParser = ctx => {
  // const header = headers[AUTH_CHECK_HEADER];
  let header = ctx.request.get(AUTH_CHECK_HEADER)
  // debug(_header, AUTH_CHECK_HEADER);
  // debug('Did we get the token?', header);
  return header ? getToken(header) : false;
}

/**
 * just return the token string
 * @param {string} header
 * @return {string} token
 */
const getToken = header => {
  return trim(header.replace(BEARER, ''))
}

/**
 * when using useJwt we allow the user to provide their own validator
 * and we pass the result to this validator for them to do further processing
 * This is useful because the user can control if they want to invalidate the client side
 * from here based on their need
 * @param {object} config configuration
 * @param {function|boolean} validator false if there is none
 * @return {function} the combine validator
 */
const createJwtValidatorChain = (config, validator = false) => {
  const jwtFn = createTokenValidator(config)
  if (!validator || typeof validator !== 'function') {
    return jwtFn;
  }
  return chainFns(jwtFn, validator)
}

/**
 * if useJwt = true then use the jsonql-jwt version
 * @param {object} config configuration
 * @param {string} type type of call
 * @param {object} contract contract.json
 * @return {function} the correct handler
 */
const getValidator = (config, type, contract) => {
  if (validatorFn && typeof validatorFn === 'function') {
    return validatorFn;
  }
  let localValidator;
  try {
    localValidator = getLocalValidator(config, type, contract)
  } catch(e) {
    // we ignore this error becasue they might not have one?
    if (!(e instanceof JsonqlResolverNotFoundError)) {
      return finalCatch(e)
    }
  }
  if (config.useJwt) {
    return createJwtValidatorChain(config, localValidator)
  }
  return localValidator;
}

/**
 * Auth middleware, we support
 * 1) OAuth 2
 * 2) JWT token
 * This is just front we don't do any real auth here,
 * instead we expect you to supply a function and pass you data and let you
 * handle the actual authentication
 * @TODO need to break this down further at the moment its very hard to debug what is going on here
 */
export default function authMiddleware(config) {
  // return middleware
  return async function(ctx, next) {
    // we should only care if there is api call involved
    const { isReq, contract } = ctx.state.jsonql;
    if (isReq && config.enableAuth) {
      try {
        const token = authHeaderParser(ctx)
        if (token) {
          debug('got a token', token)
          validatorFn = getValidator(config , AUTH_TYPE, contract)
          let userdata = await validatorFn(token)
          debug('validatorFn result', userdata)
          if (isNotEmpty(userdata) && isObject(userdata)) {
            // here we add the userData to the global
            // @TODO need more testing to see if this is going to work or not
            ctx.state.jsonql.userdata = userdata;
            // debug('get user data result', userdata)
            await next()
          } else {
            debug('throw at wrong result', userdata)
            return forbiddenHandler(ctx, {message: 'userdata is empty?'})
          }
        } else {
          debug('throw at headers not found', ctx.request.headers)
          return forbiddenHandler(ctx, {message: 'header is not found!'})
        }
      } catch(e) {
        if (e instanceof JsonqlResolverNotFoundError) {
          return ctxErrorHandler(ctx, 404, e)
        } else {
          debug('throw at some where throw error', e)
          return forbiddenHandler(ctx, e)
        }
      }
    } else {
      await next()
    }
  }
}
